package org.fengfei.lanproxy.server.config;

import java.io.*;
import java.nio.charset.Charset;
import java.util.*;

import cn.hutool.core.io.file.FileReader;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.fengfei.lanproxy.common.Config;
import org.fengfei.lanproxy.common.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.reflect.TypeToken;

/**
 * server config
 *
 * @author fengfei
 */
public class ProxyConfig implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 配置文件为config.json
     */
    public static final String CONFIG_FILE;

    private static Logger logger = LoggerFactory.getLogger(ProxyConfig.class);
    /**
     * nginxPath
     */
    private static String nginxPath = Config.getInstance().getStringValue("server.nginx.conf.path");

    static {

        // 代理配置信息存放在用户根目录下
        String dataPath = System.getProperty("user.dir") + "/conf/" + ".lanproxy/";
        File file = new File(dataPath);
        if (!file.isDirectory()) {
            file.mkdir();
        }

        CONFIG_FILE = dataPath + "/config.json";
    }


    /**
     * 代理服务器绑定主机host
     */
    private String serverBind;

    /**
     * 代理服务器与代理客户端通信端口
     */
    private Integer serverPort;

    /**
     * 配置服务绑定主机host
     */
    private String configServerBind;

    /**
     * 配置服务端口
     */
    private Integer configServerPort;

    /**
     * 配置服务管理员用户名
     */
    private String configAdminUsername;

    /**
     * 配置服务管理员密码
     */
    private String configAdminPassword;

    /**
     * 代理客户端，支持多个客户端
     */
    private List<Client> clients;

    /**
     * 更新配置后保证在其他线程即时生效
     */
    private static ProxyConfig instance = new ProxyConfig();

    /**
     * 代理服务器为各个代理客户端（key）开启对应的端口列表（value）
     */
    // clientKey：ClientProxyMapping = key:用户配置
    private volatile Map<String, List<ClientProxyMapping>> clientInetPortMapping = new HashMap<>();

    /**
     * 代理服务器上的每个对外端口（key）对应的代理客户端背后的真实服务器信息（value）
     */
    private volatile Map<String, String> domainLanInfoMapping = new HashMap<>();

    /**
     * 代理服务器上的每个对外端口（key）对应的代理客户端背后的真实服务器信息（value）
     * domain:InetPor
     */
    private volatile Map<String, Integer> domainInetPortMapping = new HashMap<>();

    /** 代理服务器上的每个对外端口（key）对应的代理客户端背后的真实服务器信息（value） */
    private volatile Map<Integer, String> inetPortLanInfoMapping = new HashMap<Integer, String>();

    // clientKey:port = key:端口号
    private volatile Map<String, List<Integer>> clientKeyInetPortMap = new HashMap<>();

    /**
     * 所有内网端口（不区分用户key）
     */
    private volatile List<Integer> allPorts = new ArrayList();

    /**
     * 配置变化监听器
     */
    private List<ConfigChangedListener> configChangedListeners = new ArrayList<ConfigChangedListener>();

    private ProxyConfig() {

        // 代理服务器主机和端口配置初始化
        this.serverPort = Config.getInstance().getIntValue("server.port");
        this.serverBind = Config.getInstance().getStringValue("server.bind", "0.0.0.0");

        // 配置服务器主机和端口配置初始化
        this.configServerPort = Config.getInstance().getIntValue("config.server.port");
        this.configServerBind = Config.getInstance().getStringValue("config.server.bind", "0.0.0.0");

        // 配置服务器管理员登录认证信息
        this.configAdminUsername = Config.getInstance().getStringValue("config.admin.username");
        this.configAdminPassword = Config.getInstance().getStringValue("config.admin.password");

        logger.info(
                "config init serverBind {}, serverPort {}, configServerBind {}, configServerPort {}, configAdminUsername {}, configAdminPassword {}",
                serverBind, serverPort, configServerBind, configServerPort, configAdminUsername, configAdminPassword);

        update(null);
    }

    public Integer getServerPort() {
        return this.serverPort;
    }

    public String getServerBind() {
        return serverBind;
    }

    public void setServerBind(String serverBind) {
        this.serverBind = serverBind;
    }

    public String getConfigServerBind() {
        return configServerBind;
    }

    public void setConfigServerBind(String configServerBind) {
        this.configServerBind = configServerBind;
    }

    public Integer getConfigServerPort() {
        return configServerPort;
    }

    public void setConfigServerPort(Integer configServerPort) {
        this.configServerPort = configServerPort;
    }

    public String getConfigAdminUsername() {
        return configAdminUsername;
    }

    public void setConfigAdminUsername(String configAdminUsername) {
        this.configAdminUsername = configAdminUsername;
    }

    public String getConfigAdminPassword() {
        return configAdminPassword;
    }

    public void setConfigAdminPassword(String configAdminPassword) {
        this.configAdminPassword = configAdminPassword;
    }

    public void setServerPort(Integer serverPort) {
        this.serverPort = serverPort;
    }

    public List<Client> getClients() {
        return clients;
    }

    /**
     * 解析配置文件
     */
    public void update(String proxyMappingConfigJson) {

        File file = new File(CONFIG_FILE);
        try {
            if (proxyMappingConfigJson == null && file.exists()) {
                InputStream in = new FileInputStream(file);
                byte[] buf = new byte[1024];
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                int readIndex;
                while ((readIndex = in.read(buf)) != -1) {
                    out.write(buf, 0, readIndex);
                }

                in.close();
                proxyMappingConfigJson = new String(out.toByteArray(), Charset.forName("UTF-8"));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        List<Client> clients = JsonUtil.json2object(proxyMappingConfigJson, new TypeToken<List<Client>>() {});
        if (clients == null) {
            clients = new ArrayList<Client>();
        }

        // TODO 切入点
        // clientKey：ClientProxyMapping = key:用户配置
        Map<String, List<ClientProxyMapping>> clientInetPortMapping = new HashMap<>();
        // domain:lan = 域名：本地ip:port
        Map<String, String> domainLanInfoMapping = new HashMap<>();

        // clientKey:port = key:端口号
        Map<String, List<Integer>> clientKeyInetPortMap = new HashMap<>();
        List<Integer> allPorts = new ArrayList<>();

        // domain:InetPor
        Map<String, Integer> domainInetPortMapping = new HashMap<>();

        Map<Integer, String> inetPortLanInfoMapping = new HashMap<Integer, String>();

        // 构造端口映射关系
        for (Client client : clients) {
            // 安全检测，如果前端处理正常不会出现此问题
            String clientKey = client.getClientKey();
            if (clientInetPortMapping.containsKey(clientKey)) {
                throw new IllegalArgumentException("密钥同时作为客户端标识，不能重复： " + clientKey);
            }
            List<ClientProxyMapping> mappings = client.getProxyMappings();

            List<ClientProxyMapping> clientProxyMappings = new ArrayList<>();
            clientInetPortMapping.put(clientKey, clientProxyMappings);

            // 端口相关

            List<Integer> inetPortList = new ArrayList<>();
            clientKeyInetPortMap.put(clientKey, inetPortList);

            // 设置初始服务端口号
            Integer setPort = 5000;
            for (ClientProxyMapping mapping : mappings) {
                if (setPort >= 65535) {
                    throw new RuntimeException("端口使用已经满了");
                }
                // 跳过8000~8999 段的端口
                if (setPort >= 8000 && setPort <= 8999) {
                    setPort = 9000;
                }

                // 增量分配一个端口（不重复）
                Integer inetPort = setPort++;
                allPorts.add(inetPort);
                inetPortList.add(inetPort);
                mapping.setInetPort(inetPort);

                // 变更为获取域名
                String domainNameFix = mapping.getDomainNameFix();
                mapping.setDomainNameFinal(domainNameFix+"."+Config.getInstance().getStringValue("server.domain2"));
                clientProxyMappings.add(mapping);

                if (domainLanInfoMapping.containsKey(domainNameFix)) {
                    throw new IllegalArgumentException("一个公网域名只能映射一个后端信息，不能重复: " + domainNameFix);
                }
                inetPortLanInfoMapping.put(inetPort, mapping.getLan());
                domainLanInfoMapping.put(domainNameFix, mapping.getLan());
                domainInetPortMapping.put(domainNameFix, inetPort);
            }
        }

        // 替换之前的配置关系
        this.clientInetPortMapping = clientInetPortMapping;
        this.domainLanInfoMapping = domainLanInfoMapping;

        this.clientKeyInetPortMap = clientKeyInetPortMap;
        this.domainInetPortMapping = domainInetPortMapping;
        this.inetPortLanInfoMapping = inetPortLanInfoMapping;
        this.clients = clients;
        this.allPorts = allPorts;

        if (!clients.isEmpty()) {
            proxyMappingConfigJson = JSONUtil.toJsonStr(clients);
            try {
                FileOutputStream out = new FileOutputStream(file);
                out.write(proxyMappingConfigJson.getBytes(Charset.forName("UTF-8")));
                out.flush();
                out.close();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        // 输出到nginx文件中
        if (StrUtil.isNotBlank(nginxPath)) {
            // 删除特定的conf文件
            delNginxConfFiles();

            // 重新生成conf文件
            saveNginxConfFiles(domainInetPortMapping);

            // 重启nginx
            try {
                String command1 = "systemctl reload nginx";
                Runtime.getRuntime().exec(command1).waitFor();

            } catch (Exception e) {
                logger.error("当前系统不支持重启nginx命令：{}","systemctl reload nginx");
            }

        } else {
            logger.error("没有找到输出位置{}", nginxPath);
        }

        notifyconfigChangedListeners();
    }

    /**
     * @param:
     * @return:
     * @Description:保存nginx配置文件
     * @Author: huhongchang
     * @Date: 2018/12/18 22:37
     */
    private void saveNginxConfFiles(Map<String, Integer> domainInetPortMapping) {
        logger.info("配置文件{}", JSONUtil.toJsonStr(domainInetPortMapping));
        for (String domainNameFix : domainInetPortMapping.keySet()) {
            Integer insetPort = domainInetPortMapping.get(domainNameFix);
            // 读取模板文件
            String domainName2 =  Config.getInstance().getStringValue("server.domain2");
            String finalDomainName = domainNameFix+"."+domainName2;
            String domainConf = readerNginxConfTemlpate(finalDomainName, insetPort);
            String filePath = StrUtil.format("{}domain-{}.conf", nginxPath,finalDomainName);
            File file = new File(filePath);
            if (!file.exists()) {
                try {
                    file.createNewFile();
                    FileOutputStream out = new FileOutputStream(file, true);
                    PrintStream p = new PrintStream(out);
                    p.println(domainConf);
                    out.close();
                    p.flush();
                    p.close();
                } catch (IOException e) {
                    logger.error("保存文件异常", e);
                }
            }
            logger.info("保存nginx配置文件");
        }
    }

    /**
     * @param:
     * @return:
     * @Description:读取nginx模版文件
     * @Author: huhongchang
     * @Date: 2018/12/18 23:55
     */
    public String readerNginxConfTemlpate(String finalDomainName, Integer port) {
        cn.hutool.core.io.file.FileReader fileReader = null;
        try {
            fileReader = new FileReader("domain-template.conf");
        } catch (Exception e) {
            return null;
        }
        String result = StrUtil.format(fileReader.readString(), finalDomainName, port);
        logger.info("读取模版文件{}", result);
        return result;
    }


    /**
     * @param:
     * @return:
     * @Description:删除nginx配置文件
     * @Author: huhongchang
     * @Date: 2018/12/18 22:36
     */
    private boolean delNginxConfFiles() {
        File nginxConfFile = new File(nginxPath);
        File[] tempFile = nginxConfFile.listFiles();
        if(tempFile==null){
            logger.error("Nginx 配置文件目录设置错误");
            return false;
        }

        for (int i = 0; i < tempFile.length; i++) {
            if (tempFile[i].getName().startsWith("domain-")) {
                logger.info("将被删除的文件名:" + tempFile[i].getName());
                boolean del = deleteFile(nginxPath + File.separator + tempFile[i].getName());
                if (del) {
                    logger.info("文件" + tempFile[i].getName() + "删除成功");
                    continue;
                }
                logger.info("文件" + tempFile[i].getName() + "删除失败");
            }
        }
        logger.info("删除历史nginx文件");
        return true;
    }

    private boolean deleteFile(String path) {
        logger.info(path);
        boolean del = false;
        File file = new File(path);
        if (file.isFile()) {
            file.delete();
            del = true;
        }
        return del;
    }

    /**
     * 配置更新通知
     */
    private void notifyconfigChangedListeners() {
        List<ConfigChangedListener> changedListeners = new ArrayList<ConfigChangedListener>(configChangedListeners);
        for (ConfigChangedListener changedListener : changedListeners) {
            changedListener.onChanged();
        }
    }

    /**
     * 添加配置变化监听器
     *
     * @param configChangedListener
     */
    public void addConfigChangedListener(ConfigChangedListener configChangedListener) {
        configChangedListeners.add(configChangedListener);
    }

    /**
     * 移除配置变化监听器
     *
     * @param configChangedListener
     */
    public void removeConfigChangedListener(ConfigChangedListener configChangedListener) {
        configChangedListeners.remove(configChangedListener);
    }

    /**
     * 获取代理客户端对应的代理服务器端口
     *
     * @param clientKey
     * @return
     */
    public List<Integer> getClientInetPorts(String clientKey) {
        return clientKeyInetPortMap.get(clientKey);
    }

    /**
     * 获取所有的clientKey
     *
     * @return
     */
    public Set<String> getClientKeySet() {
        return clientInetPortMapping.keySet();
    }

    /**
     * 根据代理服务器端口获取后端服务器代理信息
     *
     * @param port
     * @return
     */
    public String getLanInfo(Integer port) {
        return inetPortLanInfoMapping.get(port);
    }

    /**
     * 返回需要绑定在代理服务器的端口（用于用户请求）
     *
     * @return
     */
    public List<Integer> getUserPorts() {
        return allPorts;
    }

    public static ProxyConfig getInstance() {
        return instance;
    }

    /**
     * 代理客户端
     *
     * @author fengfei
     */
    public static class Client implements Serializable {

        private static final long serialVersionUID = 1L;

        /**
         * 客户端备注名称
         */
        private String name;

        /**
         * 代理客户端唯一标识key
         */
        private String clientKey;

        /**
         * 代理客户端与其后面的真实服务器映射关系
         */
        private List<ClientProxyMapping> proxyMappings;

        private int status;

        public String getClientKey() {
            return clientKey;
        }

        public void setClientKey(String clientKey) {
            this.clientKey = clientKey;
        }

        public List<ClientProxyMapping> getProxyMappings() {
            return proxyMappings;
        }

        public void setProxyMappings(List<ClientProxyMapping> proxyMappings) {
            this.proxyMappings = proxyMappings;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public int getStatus() {
            return status;
        }

        public void setStatus(int status) {
            this.status = status;
        }

    }

    /**
     * 代理客户端与其后面真实服务器映射关系
     *
     * @author fengfei
     */
    public static class ClientProxyMapping {
        /**
         * 代理服务器域名前缀
         */
        private String domainNameFix;

        /**
         * 代理服务器完整域名
         */
        private String domainNameFinal;

        /**
         * 代理服务器端口
         */
        private Integer inetPort;

        /**
         * 需要代理的网络信息（代理客户端能够访问），格式 192.168.1.99:80 (必须带端口)
         */
        private String lan;

        /**
         * 备注名称
         */
        private String name;

        public String getDomainNameFinal() {
            return domainNameFinal;
        }

        public void setDomainNameFinal(String domainNameFinal) {
            this.domainNameFinal = domainNameFinal;
        }

        public String getDomainNameFix() {
            return domainNameFix;
        }

        public void setDomainNameFix(String domainNameFix) {
            this.domainNameFix = domainNameFix;
            //setDomainNameFinal(domainNameFix+"."+Config.getInstance().getStringValue("server.domain2"));
        }

        public Integer getInetPort() {
            return inetPort;
        }

        public void setInetPort(Integer inetPort) {
            this.inetPort = inetPort;
        }

        public String getLan() {
            return lan;
        }

        public void setLan(String lan) {
            this.lan = lan;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

    }

    /**
     * 配置更新回调
     *
     * @author fengfei
     */
    public static interface ConfigChangedListener {

        void onChanged();
    }
}
